Plans of hetziner-46 improvements.md

Server: 46.62.228.173 (Hetzner, Debian 12, 16GB RAM)
Date: 2026-04-28

This bot-telegram for coach and diet apps,
Name of bot: t.me/newbothelpingmeBot

api bot tokens:
8259336570:AAFTdY3Il-HuK801BdwBkSSXf9HMJtF-Q0w

chat id: 
7674461942
———---------
The following telegram bot is for ielts app
Bot name: t.me/THE_IELTS_FAST_111Bot.

The token bot API:
8632182251:AAG338J2hjVR1GESos04VfT9Z1QdqbmBgMk

Chat_ID = 7674461942
———
For the following issues, you have to understand the whole plan and fixation code patterns and follow them, keeping your memory well and perform well according to the context without hallucination or without making errors. Make phases specs and act ase expert developer, with the following guide:


Part 1: Coach.promedic1.com — Cascading Issues Chain
The coach app has a chain of interconnected issues where one problem triggers or masks others.

Issue Chain Diagram
🔴 #1: Systemd Port CollisionNo ExecStartPre kill
Services crash-loop~19,400 restarts in 7 days
🔴 #2: Multiple Health ScriptsALL restart simultaneously
Race condition:4 scripts fight to restartthe same service
🟠 #3: Service Worker CacheStale cache-name 'v1'
Users see old codeafter deployments
🟠 #4: Registration 400 ErrorPB validation rules
Users can't create accounts
🟠 #5: Redeem-code 403Called on page loadwithout auth token
Console error spamon every page visit
🟡 #6: .bak file in /assets/Served publicly
Source code leakage
🟡 #7: .git exposedin webroot
Full repo historydownloadable
🔴 Issue #1: Systemd Port Collision Crash Loop (Root Cause of Instability)
What happens: When the service restarts (via cron, weekly restart, or hook file change), the old PocketBase process doesn't release port 8092 fast enough. The new process tries to bind → fails → systemd retries every 5 seconds → infinite crash loop.

Evidence:

Error: listen tcp 127.0.0.1:8092: bind: address already in use
19,404 restarts in 7 days for Coach alone.

Fix: Add ExecStartPre to kill orphan processes:

ini
ExecStartPre=/bin/sh -c 'fuser -k 8092/tcp 2>/dev/null; sleep 2 || true'
RestartSec=15   # was 5
KillMode=mixed
TimeoutStopSec=15
🔴 Issue #2: 4 Health Scripts Fight Over the Same Services
This is a massive problem. Four independent scripts all check PocketBase health every 3-5 minutes, and ALL of them attempt systemctl restart if health fails:

Script	Schedule	Action
/usr/local/bin/pb-health-check.sh	*/5 * * * * (crontab)	Restarts PB if health fails
/root/monitoring-agent/check-pocketbase.sh	*/3 * * * * (crontab)	Restarts PB if health fails
/usr/local/bin/service-guardian.sh	*/5 * * * * (/etc/cron.d/)	Restarts PB if health fails
/usr/local/bin/promedic-monitor	*/5 * * * * (/etc/cron.d/)	Runs fix if status fails
CAUTION

When PB is momentarily down (e.g., during a hot-reload), up to 4 scripts simultaneously try to restart it. This creates the exact port collision that causes Issue #1. Each script issues systemctl restart, which kills the process and tries to start a new one — but another script already started one 30 seconds ago.

Fix: Consolidate into ONE monitoring script. Delete the other three.

🟠 Issue #3: Service Worker Caches Stale Content
The service worker (
sw.js
) has a hardcoded cache name coach-app-v1 that never changes between deployments:

javascript
const CACHE_NAME = "coach-app-v1";  // ← Never updated
This means after a deployment, users continue seeing the old cached version. The service worker caches /index.html and all /assets/* files aggressively. Combined with Caddy's Cache-Control: public, max-age=31536000, immutable on assets, users may need to manually clear their browser data to see updates.

Fix: Use a build hash in the cache name: coach-app-v${BUILD_HASH} or implement a cache-busting strategy in the activate handler.

🟠 Issue #4: User Registration Returns 400 Bad Request
Browser test confirmed: Creating an account via the registration form fails with HTTP 400. The PocketBase users collection likely has validation rules (required fields like phone, country_code) that the registration form doesn't send.

The cascade: This blocks new users → they can't log in → they can't use premium features → the approval workflow never triggers.

🟠 Issue #5: Redeem-Code API Called on Page Load (403)
The frontend calls /pb/api/custom/redeem-code automatically on page load, but the user isn't authenticated yet (no Bearer token). The hook correctly returns 403, but this:

Spams the browser console with errors on every visit
Wastes server resources
Creates false positives in monitoring logs
Fix: The frontend should only call this endpoint when the user explicitly submits an activation code.

🟡 Issue #6: .bak File Served Publicly
A backup file exists in the assets directory and is publicly accessible:

/var/www/coach.promedic1.com/assets/SocialHub-Bmb5I9MX.js.bak (8.9KB)
This exposes source code. Caddy serves it because it's inside the webroot.

Fix: Delete it or add a Caddy rule to block *.bak files.

🟡 Issue #7: .git Directory Exposed in Webroot
The .git directory is present inside the coach webroot:

/var/www/coach.promedic1.com/.git
While Caddy has a rule blocking /.git/*, the pattern may not cover all git-related access patterns (e.g., /.git/config vs /.git).

Fix: Remove .git from webroot or add explicit Caddy block.

Part 2: Server-Wide Audit — Bad Practices & Future Bugs
🔴 Critical Issues
A. Telegram Bot Tokens Hardcoded in PocketBase Hooks
All three apps have Telegram bot tokens hardcoded directly in the JavaScript hook files:

javascript
// coach & diet hooks:
url: "https://api.telegram.org/bot8640574098:AAHmn7SCskmlx0M1YUwAy9SaNZyA9qjHkpM/sendMessage"
// ielts hooks:
url: "https://api.telegram.org/bot8632182251:AAG338J2hjVR1GESos04VfT9Z1QdqbmBgMk/sendMessage"
CAUTION

If anyone gains read access to the hooks directory (or if it's backed up to git), they get your Telegram bot tokens. These tokens allow sending messages as your bot and potentially reading updates.

Fix: Move tokens to environment variables or a secrets file (/etc/default/pocketbase-secrets).

B. IELTS Database Files Are World-Readable
-rw-r--r-- 1 root root  data.db           ← world-readable!
-rw-r--r-- 1 root root  auxiliary.db      ← world-readable!
Compare with Coach/Diet which are correctly rw------- (owner-only). Any process on the server can read the IELTS database.

Fix: chmod 600 /root/ielts-pocketbase/data/*.db*

C. .git Directories in 5 Webroots
/var/www/coach.promedic1.com/.git
/var/www/diet-plans/.git
/var/www/promedic1.com/.git
/var/www/clinical/.git
/var/www/super/.git
Even with Caddy blocking /.git/*, this exposes commit history, emails, and potentially secrets if someone finds a bypass.

Fix: Remove all .git from /var/www/ directories. Deploy code without git history.

D. Everything Runs as Root
All PocketBase services, Docker containers, Node.js services, and Python services run as root. If any of these are compromised, the attacker has full server control.

ini
User=root    # in all service files
Group=root
Fix: Create dedicated service users (pocketbase, nodeapp, etc.) and run each service under its own user with minimal permissions.

🟠 High Priority Issues
E. Cron Job Chaos — 12+ Overlapping Jobs
The cron configuration is a mess of overlapping, competing scripts from different "sessions":

Frequency	Scripts Running
Every 3 min	check-pocketbase.sh
Every 5 min	pb-health-check.sh + service-guardian.sh + promedic-monitor + system-alerts.sh + health-check.sh
Every 15 min	monitor.js --app=all
Every 30 min	tts-memory-manager.sh
Weekly (Sun 3AM)	server-maintenance.sh
Weekly (Sun 4AM)	restart diet + coach PocketBase ← conflicts with maintenance above!
WARNING

The weekly Sunday 4AM restart (systemctl restart diet-pocketbase coach-pocketbase) runs RIGHT AFTER the 3AM maintenance. This is likely the trigger for the 19,400+ crash loops — the maintenance script may not fully stop the services before the cron restart hits.

Fix: Consolidate all monitoring into ONE script. Remove duplicates.

F. Weekly PocketBase Restart Cron Only Restarts 2 of 3
cron
0 4 * * 0 root systemctl restart diet-pocketbase coach-pocketbase
IELTS PocketBase is not included in the weekly restart. If the restart is needed for memory leak prevention, all three should be restarted. If it's not needed, remove the cron entirely.

G. WAL Checkpoint Only Runs on data.db, Not auxiliary.db
The WAL checkpoint script only checkpoints data.db:

bash
for db in .../data.db .../data.db .../data.db; do
    sqlite3 "$db" "PRAGMA wal_checkpoint(TRUNCATE);"
done
But the auxiliary.db WAL files are actively growing (up to 113KB). Over time, these will bloat.

Fix: Add auxiliary.db to the checkpoint script.

H. Restic Backup Backs Up a Non-Existent Path
bash
restic backup /root/pb_data/   # ← This directory doesn't exist
The actual PB data is in /root/coach-pocketbase/pb_data/, /root/diet-pocketbase/pb_data/, and /root/ielts-pocketbase/data/. The script does backup these correct paths later (under "live PocketBase"), but the /root/pb_data/ line silently fails.

I. No IELTS PocketBase Log Rotation
The IELTS service writes logs to a file:

ini
StandardOutput=append:/root/ielts-pocketbase/pocketbase.log
StandardError=append:/root/ielts-pocketbase/pocketbase.log
But Coach and Diet don't have this (they log to journald). The IELTS log file will grow unbounded with no rotation configured.

🟡 Medium Priority Issues
J. CSP Policy Has Unnecessary Allowances
All apps have script-src 'unsafe-inline' https://cdn.tailwindcss.com in CSP, but Coach uses a pre-built Vite bundle — it doesn't load TailwindCSS from CDN. This is a leftover that weakens the security policy.

K. Docker Containers Running
Two Docker containers are running:

webapp_ielts (Node.js app on port 8093)
uptime-kuma (monitoring on port 3002)
Uptime-Kuma duplicates the monitoring already done by the 5+ cron scripts. This is resource waste.

L. Certbot Installed but Caddy Handles TLS
A certbot cron job runs every 12 hours, but Caddy manages all TLS certificates automatically. Certbot is unnecessary bloat and could potentially conflict.

M. PHP Session Cleaner Running but No PHP Apps
A PHP session cleanup cron runs every 30 minutes, but there are no PHP applications on the server.

Summary Table
#	Issue	Severity	Category	Effort
1	Port collision crash loop (no ExecStartPre)	🔴 Critical	Stability	10 min
2	4 competing health scripts cause restarts	🔴 Critical	Stability	30 min
A	Telegram tokens hardcoded in hooks	🔴 Critical	Security	20 min
B	IELTS DB world-readable	🔴 Critical	Security	2 min
C	.git in 5 webroots	🔴 Critical	Security	5 min
D	Everything runs as root	🔴 Critical	Security	1 hour
3	Service worker stale cache	🟠 High	Functionality	15 min
4	Registration 400 error	🟠 High	Functionality	20 min
5	Redeem-code 403 on page load	🟠 High	UX/Performance	Frontend fix
E	12+ overlapping cron jobs	🟠 High	Stability	45 min
F	Weekly restart skips IELTS	🟠 High	Consistency	2 min
G	WAL checkpoint misses auxiliary.db	🟠 High	Data	5 min
H	Restic backs up non-existent path	🟠 High	Backup	5 min
I	IELTS log no rotation	🟠 High	Operations	10 min
6	.bak file publicly served	🟡 Medium	Security	2 min
J	CSP has unused TailwindCDN rule	🟡 Medium	Security	5 min
K	Uptime-Kuma duplicates monitoring	🟡 Medium	Resources	10 min
L	Certbot unnecessary (Caddy handles TLS)	🟡 Medium	Bloat	5 min
M	PHP cron but no PHP apps	🟡 Medium	Bloat	2 min
Live Browser Test Recording
=============================================================


priority order:
Fix systemd port collision (Issues #1) — prevents the 19K crash loops
Consolidate monitoring (Issue #2/E) — stops competing scripts from fighting
Fix security (A, B, C) — Telegram tokens, DB perms, .git removal
Clean up cron (E, F, G, H) — remove duplicates and fix gaps

Fix Guide Part 1: Stability & Crash Prevention
**Server:** `46.62.228.173` | **SSH:** `ssh -i ~/.ssh/hetzner_dokploy root@46.62.228.173`

---

## Fix #1: Systemd Port Collision (Critical)

> [!CAUTION]
> This is the ROOT CAUSE of 19,400+ crash loops. The service tries to start before the old process releases the port.

### Current (Broken)
```ini
# /etc/systemd/system/coach-pocketbase.service
[Service]
ExecStart=/root/coach-pocketbase/pocketbase serve --http=127.0.0.1:8092 ...
Restart=always
RestartSec=5          # Too fast — port not released yet
TimeoutStopSec=30     # Too long — delays cleanup
```

### Fixed Version
```ini
# /etc/systemd/system/coach-pocketbase.service
[Service]
# ✅ Kill any orphan process holding the port BEFORE starting
ExecStartPre=/bin/sh -c 'fuser -k 8092/tcp 2>/dev/null; sleep 1 || true'

ExecStart=/root/coach-pocketbase/pocketbase serve --http=127.0.0.1:8092 \
  --dir=/root/coach-pocketbase/pb_data \
  --migrationsDir=/root/coach-pocketbase/pb_migrations \
  --hooksDir=/root/coach-pocketbase/pb_hooks \
  --origins="https://coach.promedic1.com"

Restart=always
RestartSec=15         # ✅ Give port 15s to release (was 5)
TimeoutStopSec=15     # ✅ Don't wait 30s to kill
KillMode=mixed        # ✅ SIGTERM main, SIGKILL children
KillSignal=SIGTERM    # ✅ Graceful shutdown first
```

### Apply to all 3 services:
```bash
# Coach (port 8092)
sed -i '/^\[Service\]/a ExecStartPre=/bin/sh -c '\''fuser -k 8092/tcp 2>/dev/null; sleep 1 || true'\''' \
  /etc/systemd/system/coach-pocketbase.service

# Diet (port 8091)
sed -i '/^\[Service\]/a ExecStartPre=/bin/sh -c '\''fuser -k 8091/tcp 2>/dev/null; sleep 1 || true'\''' \
  /etc/systemd/system/diet-pocketbase.service

# IELTS (port 8090)
sed -i '/^\[Service\]/a ExecStartPre=/bin/sh -c '\''fuser -k 8090/tcp 2>/dev/null; sleep 1 || true'\''' \
  /etc/systemd/system/ielts-pocketbase.service
```

> [!TIP]
> **Better approach:** Replace each service file entirely with the corrected version (shown in Fix #1 complete files below) rather than using sed.

### Complete Fixed Service Files

#### coach-pocketbase.service
```ini
[Unit]
Description=Coach PocketBase (coach.promedic1.com)
After=network.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/root/coach-pocketbase

# Kill orphan port holder before start
ExecStartPre=/bin/sh -c 'fuser -k 8092/tcp 2>/dev/null; sleep 1 || true'

ExecStart=/root/coach-pocketbase/pocketbase serve \
  --http=127.0.0.1:8092 \
  --dir=/root/coach-pocketbase/pb_data \
  --migrationsDir=/root/coach-pocketbase/pb_migrations \
  --hooksDir=/root/coach-pocketbase/pb_hooks \
  --origins="https://coach.promedic1.com"

Restart=always
RestartSec=15
TimeoutStopSec=15
KillMode=mixed
KillSignal=SIGTERM

# Resource Limits
MemoryMax=1G
MemoryHigh=512M
CPUQuota=100%
LimitNOFILE=65536
TasksMax=512

# Security Hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/root/coach-pocketbase
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target
```

#### diet-pocketbase.service
```ini
[Unit]
Description=Diet PocketBase (diet.promedic1.com)
After=network.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/root/diet-pocketbase

ExecStartPre=/bin/sh -c 'fuser -k 8091/tcp 2>/dev/null; sleep 1 || true'

ExecStart=/root/diet-pocketbase/pocketbase serve \
  --http=127.0.0.1:8091 \
  --dir=/root/diet-pocketbase/pb_data \
  --migrationsDir=/root/diet-pocketbase/pb_migrations \
  --hooksDir=/root/diet-pocketbase/pb_hooks \
  --origins="https://diet.promedic1.com"

Restart=always
RestartSec=15
TimeoutStopSec=15
KillMode=mixed
KillSignal=SIGTERM

MemoryMax=1G
MemoryHigh=512M
CPUQuota=100%
LimitNOFILE=65536
TasksMax=512

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/root/diet-pocketbase
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target
```

#### ielts-pocketbase.service
```ini
[Unit]
Description=IELTS PocketBase (ielts.fast)
After=network.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/root/ielts-pocketbase

ExecStartPre=/bin/sh -c 'fuser -k 8090/tcp 2>/dev/null; sleep 1 || true'

ExecStart=/root/ielts-pocketbase/pocketbase serve \
  --http=127.0.0.1:8090 \
  --dir=/root/ielts-pocketbase/data \
  --hooksDir=/root/ielts-pocketbase/data/pb_hooks \
  --origins="https://ielts.fast"

Restart=always
RestartSec=15
TimeoutStopSec=15
KillMode=mixed
KillSignal=SIGTERM

MemoryMax=1G
MemoryHigh=512M
CPUQuota=100%
LimitNOFILE=65536
TasksMax=512

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/root/ielts-pocketbase
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
LockPersonality=yes

# Logging (consistent with coach/diet via journald)
SyslogIdentifier=ielts-pocketbase

[Install]
WantedBy=multi-user.target
```

### Reload after applying:
```bash
systemctl daemon-reload
systemctl restart coach-pocketbase diet-pocketbase ielts-pocketbase
sleep 5
systemctl status coach-pocketbase diet-pocketbase ielts-pocketbase --no-pager
```

---

## Fix #2: Consolidate 4 Competing Health Scripts Into 1

> [!WARNING]
> Four scripts all restart PocketBase when health fails. They race each other, causing the port collisions in Fix #1.

### Delete the duplicates:
```bash
# Remove from root crontab
crontab -l | grep -v 'check-pocketbase.sh' | grep -v 'pb-health-check.sh' | crontab -

# Remove duplicate cron.d entries
sed -i '/promedic-monitor/d' /etc/cron.d/promedic
rm -f /etc/cron.d/service-guardian

# Keep ONLY one unified script
```

### The ONE monitoring script to rule them all:

```bash
#!/bin/bash
# /usr/local/bin/unified-health-monitor.sh
# ═══════════════════════════════════════════════════════
# Unified Health Monitor — SINGLE source of truth
# Replaces: pb-health-check.sh, check-pocketbase.sh,
#           service-guardian.sh, promedic-monitor
# ═══════════════════════════════════════════════════════
set -uo pipefail

LOG="/var/log/unified-health.log"
LOCK="/tmp/health-monitor.lock"
MAX_LOG_SIZE=5242880  # 5MB

# ─── Prevent concurrent runs ───
if [ -f "$LOCK" ]; then
    pid=$(cat "$LOCK" 2>/dev/null)
    if kill -0 "$pid" 2>/dev/null; then
        exit 0  # Another instance running, skip silently
    fi
fi
echo $$ > "$LOCK"
trap 'rm -f "$LOCK"' EXIT

log() { echo "[$(date -Iseconds)] $1" >> "$LOG"; }

# ─── Rotate log if too large ───
if [ -f "$LOG" ] && [ "$(stat -f%z "$LOG" 2>/dev/null || stat -c%s "$LOG" 2>/dev/null)" -gt "$MAX_LOG_SIZE" ]; then
    mv "$LOG" "${LOG}.old"
    log "Log rotated"
fi

# ─── Check systemd services ───
SERVICES=("caddy" "ielts-pocketbase" "diet-pocketbase" "coach-pocketbase" "ielts-tts" "ielts-analysis" "telegram-notifier")
ISSUES=0

for svc in "${SERVICES[@]}"; do
    if ! systemctl is-active --quiet "$svc" 2>/dev/null; then
        log "⚠️  $svc is DOWN. Attempting restart..."
        systemctl restart "$svc" 2>/dev/null
        sleep 5
        if systemctl is-active --quiet "$svc"; then
            log "✅ $svc recovered"
        else
            log "❌ $svc FAILED to restart"
            ((ISSUES++))
        fi
    fi
done

# ─── Check PocketBase HTTP health ───
PB_INSTANCES="8090:ielts-pocketbase 8091:diet-pocketbase 8092:coach-pocketbase"
for entry in $PB_INSTANCES; do
    IFS=':' read -r port service <<< "$entry"
    HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
        --max-time 10 "http://127.0.0.1:${port}/api/health" 2>/dev/null || echo "000")

    if [ "$HTTP_CODE" != "200" ]; then
        # Don't restart if systemd already restarted above
        if systemctl is-active --quiet "$service" 2>/dev/null; then
            log "⚠️  $service running but HTTP $HTTP_CODE on port $port. Restarting..."
            systemctl restart "$service" 2>/dev/null
            sleep 8  # ✅ Wait longer than RestartSec (15s would be ideal)
        fi
    fi
done

# ─── Check Docker containers ───
for container in webapp_ielts uptime-kuma; do
    if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${container}$"; then
        continue
    fi
    log "⚠️  Container $container is down. Restarting..."
    docker restart "$container" 2>/dev/null
    ((ISSUES++))
done

if [ "$ISSUES" -eq 0 ]; then
    # Only log OK every 30 minutes (not every 5 min)
    MINUTE=$(date +%M)
    if [ "$((MINUTE % 30))" -lt 5 ]; then
        log "✅ All services healthy"
    fi
fi
```

### Install the unified script:
```bash
# Make executable
chmod +x /usr/local/bin/unified-health-monitor.sh

# Single crontab entry replaces all 4
cat > /etc/cron.d/unified-monitor << 'EOF'
# Unified Health Monitor — runs every 5 minutes
# Replaces: pb-health-check, check-pocketbase, service-guardian, promedic-monitor
*/5 * * * * root /usr/local/bin/unified-health-monitor.sh
EOF

# Clean up old scripts (don't delete, archive)
mkdir -p /root/archived-scripts
mv /usr/local/bin/pb-health-check.sh /root/archived-scripts/ 2>/dev/null
mv /usr/local/bin/service-guardian.sh /root/archived-scripts/ 2>/dev/null
mv /usr/local/bin/health-check.sh /root/archived-scripts/ 2>/dev/null
```

---

## Fix #3: Fix Weekly Restart Race Condition

### Current (Broken)
```cron
# /etc/cron.d/promedic — line 12
0 4 * * 0 root systemctl restart diet-pocketbase coach-pocketbase
```

**Problems:** (1) Runs right after 3AM maintenance, (2) skips IELTS, (3) restarts both simultaneously.

### Fixed Version
```cron
# /etc/cron.d/promedic — Weekly graceful restart (staggered)
# Restart one at a time with 30s gaps to avoid port collisions
0 5 * * 0 root systemctl restart ielts-pocketbase && sleep 30 && systemctl restart diet-pocketbase && sleep 30 && systemctl restart coach-pocketbase
```

---

## Fix #4: WAL Checkpoint — Include auxiliary.db

### Current (Incomplete)
```bash
# /usr/local/bin/pb-wal-checkpoint.sh
for db in .../data.db .../data.db .../data.db; do
    sqlite3 "$db" "PRAGMA wal_checkpoint(TRUNCATE);"
done
```

### Fixed Version
```bash
#!/bin/bash
# /usr/local/bin/pb-wal-checkpoint.sh
# Checkpoint ALL SQLite databases (data + auxiliary)

DATABASES=(
    "/root/ielts-pocketbase/data/data.db"
    "/root/ielts-pocketbase/data/auxiliary.db"
    "/root/diet-pocketbase/pb_data/data.db"
    "/root/diet-pocketbase/pb_data/auxiliary.db"
    "/root/coach-pocketbase/pb_data/data.db"
    "/root/coach-pocketbase/pb_data/auxiliary.db"
)

for db in "${DATABASES[@]}"; do
    if [ -f "$db" ]; then
        sqlite3 "$db" "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null
    fi
done
```

Fix Guide Part 2: Security & Cleanup
**Server:** `46.62.228.173` | **SSH:** `ssh -i ~/.ssh/hetzner_dokploy root@46.62.228.173`

---

## Fix #5: Extract Telegram Bot Tokens from Hooks

> [!CAUTION]
> Bot tokens are hardcoded in JS files that get backed up to git and restic. Anyone with access sees them.

### Current (Insecure)
```javascript
// /root/coach-pocketbase/pb_hooks/premium-features.pb.js
$http.send({
    url: "https://api.telegram.org/bot8640574098:AAHmn7SCskmlx0M1YUwAy9SaNZyA9qjHkpM/sendMessage",
    method: "POST",
    // ...
});
```

### Step 1: Create secrets file
```bash
cat > /etc/default/pocketbase-secrets << 'EOF'
# Telegram Bot Configuration
COACH_DIET_BOT_TOKEN=8640574098:AAHmn7SCskmlx0M1YUwAy9SaNZyA9qjHkpM
COACH_DIET_CHAT_ID=7674461942
IELTS_BOT_TOKEN=8632182251:AAG338J2hjVR1GESos04VfT9Z1QdqbmBgMk
IELTS_CHAT_ID=7674461942
EOF
chmod 600 /etc/default/pocketbase-secrets
```

### Step 2: Add EnvironmentFile to each service
```ini
# Add to [Service] section of all 3 PocketBase services:
EnvironmentFile=/etc/default/pocketbase-secrets
```

### Step 3: Update hooks to read from environment
```javascript
// /root/coach-pocketbase/pb_hooks/premium-features.pb.js
// ✅ Read token from environment variable
const BOT_TOKEN = $os.getenv("COACH_DIET_BOT_TOKEN");
const CHAT_ID = $os.getenv("COACH_DIET_CHAT_ID");

$http.send({
    url: `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`,
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
        chat_id: CHAT_ID,
        text: text,
        parse_mode: "Markdown"
    })
});
```

> [!NOTE]
> PocketBase v0.25 supports `$os.getenv()` in hooks. This reads the environment variables injected by systemd's `EnvironmentFile`.

### Same pattern for IELTS:
```javascript
// /root/ielts-pocketbase/data/pb_hooks/premium-features.pb.js
const BOT_TOKEN = $os.getenv("IELTS_BOT_TOKEN");
const CHAT_ID = $os.getenv("IELTS_CHAT_ID");
```

---

## Fix #6: IELTS Database Permissions (2-minute fix)

### Current (World-readable)
```
-rw-r--r-- root root  data.db         ← ANY process can read user data
-rw-r--r-- root root  auxiliary.db    ← ANY process can read sessions
```

### Fix
```bash
chmod 600 /root/ielts-pocketbase/data/*.db*
# Verify:
ls -la /root/ielts-pocketbase/data/*.db*
# Should show: -rw------- (owner only)
```

---

## Fix #7: Remove .git from All Webroots

### Current (5 exposed repos)
```
/var/www/coach.promedic1.com/.git    ← commit history, emails, secrets
/var/www/diet-plans/.git
/var/www/promedic1.com/.git
/var/www/clinical/.git
/var/www/super/.git
```

### Fix
```bash
# Remove .git from all web-accessible directories
for dir in /var/www/coach.promedic1.com \
           /var/www/diet-plans \
           /var/www/promedic1.com \
           /var/www/clinical \
           /var/www/super; do
    if [ -d "$dir/.git" ]; then
        rm -rf "$dir/.git"
        echo "Removed .git from $dir"
    fi
    # Also remove .gitignore
    rm -f "$dir/.gitignore"
done
```

### Also add global Caddy block (defense in depth):

Add to the **top** of `/etc/caddy/Caddyfile` before any site blocks:

```caddyfile
# Global options — block dangerous files across ALL sites
(security_blocks) {
    @blocked {
        path /.git /.git/* /.gitignore
        path /.env /.env.*
        path /*.bak /*.sql /*.tar /*.tar.gz /*.zip
        path /docker-compose* /Dockerfile
    }
    respond @blocked 404
}
```

Then in each site block add:
```caddyfile
coach.promedic1.com {
    import security_blocks
    # ... rest of config
}
```

---

## Fix #8: Remove .bak File from Coach Assets

```bash
rm -f /var/www/coach.promedic1.com/assets/SocialHub-Bmb5I9MX.js.bak
rm -f /var/www/html/index.nginx-debian.html.bak
```

---

## Fix #9: Fix Restic Backup Non-Existent Path

### Current (Silent failure)
```bash
# /usr/local/bin/restic-backup.sh — line referencing ghost path
restic backup /root/pb_data/   # ← DOES NOT EXIST
```

### Fix — Remove the ghost path
```bash
# Edit the script and remove/comment the /root/pb_data/ line
sed -i 's|/root/pb_data/|# REMOVED: /root/pb_data/ (does not exist)|g' \
    /usr/local/bin/restic-backup.sh
```

The correct paths (`/root/ielts-pocketbase/`, `/root/diet-pocketbase/`, `/root/coach-pocketbase/`) are already backed up later in the same script under "live PocketBase."

---

## Fix #10: IELTS Log Rotation

### Current — No rotation, log grows forever
```ini
# ielts-pocketbase.service
StandardOutput=append:/root/ielts-pocketbase/pocketbase.log
StandardError=append:/root/ielts-pocketbase/pocketbase.log
```

### Fix — Remove file logging, use journald (consistent with Coach/Diet)
The fixed service file in Part 1 already removes `StandardOutput` and `StandardError` directives, so logs go to journald like Coach and Diet.

### Alternative — If you want to keep file logging, add logrotate:
```bash
cat > /etc/logrotate.d/ielts-pocketbase << 'EOF'
/root/ielts-pocketbase/pocketbase.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
    size 10M
}
EOF
```

---

## Fix #11: Clean Up Unnecessary Bloat

### Remove certbot (Caddy handles TLS)
```bash
# Check if certbot is actually managing any certs
certbot certificates 2>/dev/null
# If empty or all handled by Caddy:
apt remove --purge certbot -y 2>/dev/null
rm -f /etc/cron.d/certbot
```

### Disable PHP session cleaner (no PHP apps)
```bash
rm -f /etc/cron.d/php
```

---

## Fix #12: CSP Policy Cleanup for Coach

### Current (Allows unused CDN)
```caddyfile
Content-Security-Policy "... script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; ... connect-src 'self'; ..."
```

Coach uses pre-built Vite bundles — no CDN TailwindCSS needed. Also `connect-src 'self'` is correct since all API calls go to same origin via `/pb/api/`.

### Fixed
```caddyfile
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self';"
```

> [!TIP]
> Remove `https://cdn.tailwindcss.com` from ALL site blocks that don't actually use CDN Tailwind. Only keep it for sites that genuinely load Tailwind from CDN (check each site's `index.html` for `<script src="https://cdn.tailwindcss.com">`).

---

## Fix #13: Cron Cleanup — Final State

After applying all fixes, the crontab should look like:

### Root crontab (`crontab -e`)
```cron
# ═══════════════════════════════════════════════════
# Root Crontab — Clean Version
# ═══════════════════════════════════════════════════

# AI Monitoring Agent (screenshots + visual checks)
*/15 * * * * cd /root/monitoring-agent && /usr/bin/node monitor.js --app=all >> /root/monitoring-agent/logs/cron.log 2>&1

# IELTS PB backup (daily 3 AM)
0 3 * * * /root/ielts-pocketbase/backup.sh >> /root/ielts-pocketbase/logs/backup.log 2>&1

# Restic offsite backup (daily 4 AM)
0 4 * * * /usr/local/bin/restic-backup.sh

# System alerts
*/5 * * * * /usr/local/bin/system-alerts.sh

# WAL checkpoint (daily 2:30 AM)
30 2 * * * /usr/local/bin/pb-wal-checkpoint.sh

# Weekly maintenance (Sunday 4 AM)
0 4 * * 0 /usr/local/bin/weekly-maintenance.sh
```

### /etc/cron.d/ directory — only keep:
```
/etc/cron.d/unified-monitor        ← The ONE health script (Fix #2)
/etc/cron.d/monitoring-cleanup     ← Screenshot cleanup
/etc/cron.d/server-maintenance     ← TTS memory + weekly maintenance
/etc/cron.d/e2scrub_all            ← System default
/etc/cron.d/mdadm                  ← System default
```

### Delete these:
```bash
rm -f /etc/cron.d/promedic           # Replaced by unified-monitor
rm -f /etc/cron.d/service-guardian   # Replaced by unified-monitor
rm -f /etc/cron.d/certbot            # Caddy handles TLS
rm -f /etc/cron.d/php                # No PHP apps
```

---

## Verification Checklist

After applying all fixes, verify:

```bash
# 1. Services running
systemctl status coach-pocketbase diet-pocketbase ielts-pocketbase --no-pager

# 2. Health endpoints
for p in 8090 8091 8092; do
    echo "Port $p: $(curl -sf http://127.0.0.1:$p/api/health | jq -r .message)"
done

# 3. No .git in webroots
find /var/www -name '.git' -type d   # Should return empty

# 4. DB permissions
ls -la /root/ielts-pocketbase/data/*.db   # Should show -rw-------

# 5. No competing crons
crontab -l | grep -c 'health\|monitor\|guardian'   # Should be ≤ 2

# 6. Secrets file protected
ls -la /etc/default/pocketbase-secrets   # Should show -rw-------

# 7. Port collision protection
grep 'ExecStartPre' /etc/systemd/system/*-pocketbase.service
# Should show fuser -k for each
```
-------------------------------------------------------------------

# Pro Review & Recommendations
**Server:** `46.62.228.173` | **Reviewed:** 2026-04-28

---

## Overall Grade: B+

The infrastructure fixes are **solid and well-executed**. The server is dramatically healthier than before. However, a **critical live bug** remains — user login/registration is broken on coach.promedic1.com due to a Caddy routing conflict.

---

## Fix-by-Fix Verification & Grading

### Stability Fixes

| # | Fix | Grade | Verified | Notes |
|---|-----|-------|----------|-------|
| 1 | Port collision (ExecStartPre) | ✅ **A** | `fuser -k` + `sleep 2` on all 3 | Solid. Two-line ExecStartPre is belt-and-suspenders |
| 2 | Unified health monitor | ✅ **A** | Lock file, log rotation, single script | Excellent. Old scripts archived, not deleted |
| 3 | Weekly restart race | ✅ **A** | Competing crons removed | Clean |
| 4 | WAL checkpoint | ✅ **A** | Now covers all 6 databases | Correct |

### Security Fixes

| # | Fix | Grade | Verified | Notes |
|---|-----|-------|----------|-------|
| 5 | Telegram tokens | ✅ **A** | 0 hardcoded, `$os.getenv()` in all hooks | Secrets file chmod 600 ✓ |
| 6 | IELTS DB perms | ✅ **A** | `-rw-------` confirmed | One command, big impact |
| 7 | .git removal | ✅ **A** | 0 .git dirs in /var/www | Clean |
| 8 | .bak removal | ✅ **A** | Deleted | Clean |
| 9 | Restic ghost path | ✅ **B** | Commented out | Should've been deleted, not commented |
| 10 | IELTS log rotation | ✅ **A** | Now uses journald | Consistent with other services |
| 11 | Bloat cleanup | ✅ **A** | certbot + PHP cron removed | Good |
| 12 | CSP cleanup | ✅ **A** | 0 TailwindCDN refs in Caddyfile | Tightened |
| 13 | Cron cleanup | ✅ **A** | Clean crontab + 5 cron.d files only | Very clean |

### Frontend Fixes

| # | Fix | Grade | Verified | Notes |
|---|-----|-------|----------|-------|
| SW | Cache versioning | ✅ **A** | Dynamic `DEPLOY_VERSION`, purge old caches | Well done |
| Reg | createRule fix | ⚠️ **B-** | Rule is `''` but **login still 404s** (see below) | Partial fix |
| Auth | Bearer token injection | ✅ **B+** | 2 occurrences patched in bundle | Works but fragile — any redeploy erases it |
| Patch | patch-coach-frontend.sh | ✅ **A-** | Exists, executable, tested | Good future-proofing |

---

## 🔴 Critical Bug Found: Login & Registration Return 404

> [!CAUTION]
> **User authentication is broken.** Both login and registration fail with 404 when called through Caddy. This means NO users can log in or create accounts on coach.promedic1.com right now.

### Root Cause: Caddy Route Ordering Conflict

The frontend uses base URL `P="/pb"`, so auth calls go to:
```
POST /pb/api/collections/users/auth-with-password
```

The Caddyfile has a **route conflict** — the `@authWithPassword` matcher at the top catches this path, but it proxies **without stripping the `/pb` prefix**:

```caddyfile
# Line 7-13 in coach block — MATCHES FIRST
@authWithPassword {
    method POST
    path /pb/api/collections/users/auth-with-password
}
handle @authWithPassword {
    reverse_proxy 127.0.0.1:8092   # ← Sends /pb/api/... to PocketBase
}
```

PocketBase receives the request as `/pb/api/collections/users/auth-with-password` — but it doesn't know what `/pb` is. It only understands `/api/...`. So it returns 404.

Meanwhile, the `handle_path /pb/*` block below **does** strip the `/pb` prefix, but it never gets a chance to handle POST auth requests because `@authWithPassword` catches them first.

### Proof

```bash
# Direct to PB (no Caddy) — works:
curl -X POST http://127.0.0.1:8092/api/collections/users/auth-with-password
# → HTTP 400 "Failed to authenticate" (correct — no valid credentials)

# Via Caddy /api/ (no /pb prefix) — works:
curl -X POST https://coach.promedic1.com/api/collections/users/auth-with-password  
# → HTTP 400 "Failed to authenticate" (correct)

# Via Caddy /pb/api/ — BROKEN:
curl -X POST https://coach.promedic1.com/pb/api/collections/users/auth-with-password
# → HTTP 404 "Resource wasn't found" ← BUG
```

### Fix

The `@authWithPassword` handler needs to strip `/pb` before proxying. Two options:

**Option A** (Simplest — remove the matcher, let `handle_path /pb/*` handle everything):
```caddyfile
# DELETE the entire @authWithPassword block (lines 7-13)
# The handle_path /pb/* already handles all /pb/* requests correctly
```

**Option B** (Keep rate-limit matcher, fix the rewrite):
```caddyfile
@authWithPassword {
    method POST
    path /pb/api/collections/users/auth-with-password
}
handle @authWithPassword {
    uri strip_prefix /pb
    reverse_proxy 127.0.0.1:8092 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
    }
}
```

> [!IMPORTANT]
> **This same bug likely affects diet.promedic1.com too** — it has an identical `@authWithPassword` block. Check and fix both.

---

## Pro Recommendations

### 🏆 Priority 1: Fix the Auth Routing (Immediate)

The auth 404 bug makes the app unusable. Fix the Caddyfile and reload:

```bash
# Edit Caddyfile, fix the @authWithPassword blocks for coach AND diet
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
```

### 🏆 Priority 2: Source Code Repo for Coach App

The current situation — patching minified production bundles with `sed` — is a ticking time bomb. Any redeployment from the original source will erase all frontend fixes.

**Recommendation:** Create a proper source repo with the fixes baked in:
- Copy `/root/trae_workouts/` as the baseline
- Apply the auth header fixes in the actual React source (not the built bundle)
- Use the Vite SW plugin that was already added
- Set up a simple deploy script: `npm run build && rsync dist/ /var/www/coach.promedic1.com/`

### 🏆 Priority 3: Monitoring Improvement

The unified monitor is great, but add **alerting** so you know when something breaks:

```bash
# Add to unified-health-monitor.sh after ISSUES check:
if [ "$ISSUES" -gt 0 ]; then
    source /etc/default/pocketbase-secrets
    curl -sf -X POST "https://api.telegram.org/bot${COACH_DIET_BOT_TOKEN}/sendMessage" \
        -d "chat_id=${COACH_DIET_CHAT_ID}" \
        -d "text=🚨 Server Alert: $ISSUES services needed restart" \
        -d "parse_mode=Markdown" > /dev/null 2>&1
fi
```

### 🏆 Priority 4: Test the Diet and IELTS Apps Too

The same Caddy routing bug almost certainly affects `diet.promedic1.com`. IELTS (`ielts.fast`) may also have it. Both need the same auth-path fix.

### 🟡 Nice-to-Have: Non-Root Service Users

Everything still runs as root. This is the biggest remaining security gap. Create dedicated users:

```bash
useradd -r -s /usr/sbin/nologin -d /root/coach-pocketbase pocketbase-coach
chown -R pocketbase-coach: /root/coach-pocketbase
# Update service: User=pocketbase-coach
```

---

## Summary

| Area | Before | After | Remaining |
|------|--------|-------|-----------|
| **Crash loops** | 19,400 restarts/week | 0 restarts in 2h | ✅ Resolved |
| **Monitoring** | 4 competing scripts | 1 unified script | ✅ Resolved |
| **Secrets** | Hardcoded in JS | Environment vars | ✅ Resolved |
| **Security** | .git exposed, world-readable DB | Cleaned | ✅ Resolved |
| **Auth routing** | — | **404 on /pb/api/auth** | 🔴 **NEW — Fix ASAP** |
| **Frontend patches** | — | Bundle patched | ⚠️ Fragile (redeploy erases) |
| **Root user** | Everything as root | Everything as root | 🟡 Future improvement |
====================================================================
----------------------------------------------------------------------


# Fix Plan: Two Remaining Issues

---

## Fix 1: Unblock User Registration on Diet & IELTS

### The Problem

PocketBase `users` collection has `createRule = '1 = 2'` which **always evaluates to false**, blocking all public registration. Coach was already fixed (empty string = allow). Diet and IELTS were missed.

| App | Current `createRule` | Effect |
|-----|---------------------|--------|
| Coach | `''` (empty) | ✅ Public registration works |
| Diet | `'1 = 2'` | ❌ Always denied |
| IELTS | `'1 = 2'` | ❌ Always denied |

### The Fix

```bash
# ── Diet: Allow public registration ──
sqlite3 /root/diet-pocketbase/pb_data/data.db \
  "UPDATE _collections SET createRule = '' WHERE name = 'users';"

# ── IELTS: Allow public registration ──
sqlite3 /root/ielts-pocketbase/data/data.db \
  "UPDATE _collections SET createRule = '' WHERE name = 'users';"
```

> [!IMPORTANT]
> PocketBase caches collection rules in memory. The services must be restarted for the change to take effect:

```bash
systemctl restart diet-pocketbase
systemctl restart ielts-pocketbase
```

### Verify

```bash
# Confirm DB values
echo "Diet:" && sqlite3 /root/diet-pocketbase/pb_data/data.db \
  "SELECT name, createRule FROM _collections WHERE name='users';"
echo "IELTS:" && sqlite3 /root/ielts-pocketbase/data/data.db \
  "SELECT name, createRule FROM _collections WHERE name='users';"
# Expected output for both: users|
# (name followed by pipe, then empty = public)

# Test registration via Caddy
curl -sk -w '\nHTTP: %{http_code}' -X POST \
  'https://diet.promedic1.com/pb/api/collections/users/records' \
  -H 'Content-Type: application/json' \
  -d '{"email":"regtest_diet@example.com","password":"TestPass123!","passwordConfirm":"TestPass123!","name":"DietTest"}'
# Expected: HTTP 200 with user record JSON

curl -sk -w '\nHTTP: %{http_code}' -X POST \
  'https://ielts.fast/pb/api/collections/users/records' \
  -H 'Content-Type: application/json' \
  -d '{"email":"regtest_ielts@example.com","password":"TestPass123!","passwordConfirm":"TestPass123!","name":"IeltsTest"}'
# Expected: HTTP 200 with user record JSON

# Clean up test users afterward
sqlite3 /root/diet-pocketbase/pb_data/data.db \
  "DELETE FROM users WHERE email='regtest_diet@example.com';"
sqlite3 /root/ielts-pocketbase/data/data.db \
  "DELETE FROM users WHERE email='regtest_ielts@example.com';"
```

> [!NOTE]
> If Diet or IELTS are meant to be **invite-only** (admin creates accounts manually), keep `createRule = '1 = 2'` and skip this fix. The coach app already allows public registration — match the same pattern only if these apps should too.

---

## Fix 2: Clean Caddy Startup Warnings

### The Problem

Caddy logs 12 warnings every time it starts. There are two categories:

**Category A — Redundant `header_up` directives (10 warnings):**
Caddy v2 automatically forwards `X-Forwarded-For` and `X-Forwarded-Proto` to upstreams. Explicitly setting them causes warnings.

**Category B — Formatting (1 warning):**
The Caddyfile has inconsistent whitespace.

### The Fix

```bash
# ── Remove redundant header_up lines ──
# These are on lines: 70,71, 446,447, 468,469, 576,577, 598,599
# Caddy does this automatically — keeping them causes warnings

sed -i '/header_up X-Forwarded-For {remote_host}/d' /etc/caddy/Caddyfile
sed -i '/header_up X-Forwarded-Proto {scheme}/d' /etc/caddy/Caddyfile

# ── Keep X-Forwarded-Prefix (Caddy does NOT do this automatically) ──
# Lines with header_up X-Forwarded-Prefix "/pb" must stay — they are needed
# for PocketBase to generate correct URLs when behind /pb/ prefix

# ── Also keep header_up Host {host} and X-Real-IP ──
# These are fine and don't generate warnings

# ── Auto-format the Caddyfile ──
caddy fmt --overwrite /etc/caddy/Caddyfile

# ── Validate before reload ──
caddy validate --config /etc/caddy/Caddyfile
# Expected: "Valid configuration"

# ── Reload (zero-downtime) ──
systemctl reload caddy
```

### Verify

```bash
# Check warnings are gone
journalctl -u caddy --since "1 minute ago" --no-pager | grep -i 'warn'
# Expected: Only "admin endpoint disabled" and HTTP-only server warnings
# (those are normal — the HTTP→HTTPS redirect server doesn't need TLS)

# Confirm X-Forwarded-Prefix still present
grep 'X-Forwarded-Prefix' /etc/caddy/Caddyfile
# Expected: 3 lines (one per PB instance)

# Confirm no redundant lines remain
grep 'X-Forwarded-For\|X-Forwarded-Proto' /etc/caddy/Caddyfile
# Expected: 0 lines

# Test all sites still work
for url in "https://coach.promedic1.com/" \
           "https://diet.promedic1.com/" \
           "https://ielts.fast/" \
           "https://promedic1.com/"; do
    printf "%-40s %s\n" "$url" "$(curl -sk -o /dev/null -w '%{http_code}' $url)"
done
# Expected: All 200
```

---

## One-Shot Script (Both Fixes Combined)

```bash
#!/bin/bash
# /root/fix-remaining-issues.sh
set -euo pipefail

echo "=== Fix 1: Unblock Diet & IELTS Registration ==="
sqlite3 /root/diet-pocketbase/pb_data/data.db \
  "UPDATE _collections SET createRule = '' WHERE name = 'users';"
echo "✅ Diet createRule updated"

sqlite3 /root/ielts-pocketbase/data/data.db \
  "UPDATE _collections SET createRule = '' WHERE name = 'users';"
echo "✅ IELTS createRule updated"

systemctl restart diet-pocketbase
sleep 3
systemctl restart ielts-pocketbase
sleep 3
echo "✅ Services restarted"

echo ""
echo "=== Fix 2: Clean Caddy Warnings ==="
sed -i '/header_up X-Forwarded-For {remote_host}/d' /etc/caddy/Caddyfile
sed -i '/header_up X-Forwarded-Proto {scheme}/d' /etc/caddy/Caddyfile
caddy fmt --overwrite /etc/caddy/Caddyfile
echo "✅ Redundant headers removed, Caddyfile formatted"

caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
echo "✅ Caddy reloaded"

echo ""
echo "=== Verification ==="
echo "Diet createRule: $(sqlite3 /root/diet-pocketbase/pb_data/data.db \
  "SELECT createRule FROM _collections WHERE name='users';")"
echo "IELTS createRule: $(sqlite3 /root/ielts-pocketbase/data/data.db \
  "SELECT createRule FROM _collections WHERE name='users';")"

for p in 8090 8091 8092; do
    echo "Port $p: $(curl -sf --max-time 5 http://127.0.0.1:$p/api/health | grep -o '"message":"[^"]*"')"
done

echo ""
echo "Caddy X-Forwarded warnings: $(journalctl -u caddy --since '30 seconds ago' --no-pager | grep -c 'Unnecessary header_up')"
echo ""
echo "✅ All fixes applied and verified"
```

### Run it:
```bash
chmod +x /root/fix-remaining-issues.sh
/root/fix-remaining-issues.sh